1 /*
2 * Scope: a generic MVC framework.
3 * Copyright (c) 2000-2002, The Scope team
4 * All rights reserved.
5 *
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
9 * are met:
10 *
11 * Redistributions of source code must retain the above copyright
12 * notice, this list of conditions and the following disclaimer.
13 *
14 * Redistributions in binary form must reproduce the above copyright
15 * notice, this list of conditions and the following disclaimer in the
16 * documentation and/or other materials provided with the distribution.
17 *
18 * Neither the name "Scope" nor the names of its contributors
19 * may be used to endorse or promote products derived from this software
20 * without specific prior written permission.
21 *
22 *
23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
28 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 *
35 *
36 * $Id: ScopeServlet.java,v 1.16 2002/09/06 16:11:48 ludovicc Exp $
37 */
38 package org.scopemvc.controller.servlet;
39
40
41 import java.io.IOException;
42 import java.util.Enumeration;
43 import java.util.HashMap;
44 import java.util.Iterator;
45 import java.util.List;
46 import javax.servlet.ServletException;
47 import javax.servlet.http.HttpServlet;
48 import javax.servlet.http.HttpServletRequest;
49 import javax.servlet.http.HttpServletResponse;
50 import org.apache.commons.logging.Log;
51 import org.apache.commons.logging.LogFactory;
52 import org.scopemvc.core.Control;
53 import org.scopemvc.core.Controller;
54 import org.scopemvc.util.BasicObjectPool;
55 import org.scopemvc.util.Debug;
56 import org.scopemvc.util.ObjectPool;
57 import org.scopemvc.util.PoolableObjectFactory;
58 import org.scopemvc.util.ScopeConfig;
59 import org.scopemvc.view.servlet.Page;
60 import org.scopemvc.view.servlet.ServletView;
61 import org.scopemvc.controller.basic.BasicController;
62 import org.scopemvc.controller.basic.ViewContext;
63
64 /***
65 * <P>
66 *
67 * Base class for a web app's servlet dispatcher: subclass this to implement
68 * application startup and initialisation (use a static initializer). This class
69 * accepts incoming requests, collects the parameters into a mutable HashMap,
70 * parses them to create a Control and to find a ViewID to identify the View the
71 * user interacted with. The request parameters are then passed to the View to
72 * populate its Model, before the View issues the Control for the owning
73 * Controller to handle. </P> <P>
74 *
75 * A configurable number of Application Controllers (and sub-Controllers and
76 * their Views and Models) are created on the first request. These are put into
77 * a pool to be shared between all future requests. For this reason, Controllers
78 * that are shared must be aware of the possible need to reset their model's
79 * state before handling a Control. </P> <P>
80 *
81 * A form request is handled as follows:<BR />
82 * <A HREF="../../../../../images/api/ScopeServlet.doPost.gif"> <IMG
83 * SRC="../../../../../images/api/ScopeServlet.doPost.gif" WIDTH="240"
84 * HEIGHT="240"> </A> </P> <P>
85 *
86 * Most steps in this sequence are implemented by Template Methods that can be
87 * overridden to change the default behaviour. </P> <P>
88 *
89 * The issue of session state management and model scope is not resolved here.
90 * </P> <P>
91 *
92 * See the various XML/XSLT and JSP servlet samples for examples of use. </P>
93 *
94 * @author <A HREF="mailto:smeyfroi@users.sourceforge.net">Steve Meyfroidt</A>
95 * @created 05 August 2002
96 * @version $Revision: 1.16 $ $Date: 2002/09/06 16:11:48 $
97 * @see ServletContext
98 * @see org.scopemvc.controller.servlet.xml.XSLScopeServlet
99 * @see org.scopemvc.controller.servlet.jsp.JSPScopeServlet
100 */
101 public abstract class ScopeServlet extends HttpServlet {
102
103 /***
104 * The default validation error handler in {@link #handleValidationFailures}
105 * puts the list of {@link org.scopemvc.view.servlet.ValidationFailure}s in
106 * the properties of the {@link org.scopemvc.controller.basic.ViewContext}
107 * under this key for later retrieval by a Controller.
108 */
109 public final static String VALIDATION_FAILURES = "org.scopemvc.controller.servlet.ValidationFailures";
110
111 /***
112 * The key used to identify the Control ID in the request parameters for
113 * this implementation. Initialised from the
114 * "org.scopemvc.controller.servlet.ScopeServlet.ControlParam" value from
115 * ScopeConfig.
116 *
117 * @see #createControl
118 */
119 public static String CONTROL_PARAM;
120
121 /***
122 * The request's key that identifies the View ID used to find the active
123 * View that a Control was sent from. Initialised from the
124 * "org.scopemvc.controller.servlet.ScopeServlet.ViewIDParam" value of
125 * ScopeConfig.
126 *
127 * @see #findPageByID
128 */
129 public static String VIEW_ID_PARAM;
130
131 private final static Log LOG = LogFactory.getLog(ScopeServlet.class);
132
133 /***
134 * Pool of shared application Controllers for this servlet instance.
135 */
136 protected ObjectPool sharedControllerPool = null;
137
138
139 /***
140 * Constructor for the ScopeServlet object
141 */
142 public ScopeServlet() {
143 // Initialise statics from config. Do it this way rather than
144 // in static initializers to allow custom ScopeConfigs to be
145 // installed and used.
146 VIEW_ID_PARAM = ScopeConfig.getString("org.scopemvc.controller.servlet.ScopeServlet.ViewIDParam");
147 if (VIEW_ID_PARAM == null) {
148 LOG.fatal("No ViewIDParam in config.");
149 }
150
151 CONTROL_PARAM = ScopeConfig.getString("org.scopemvc.controller.servlet.ScopeServlet.ControlParam");
152 if (CONTROL_PARAM == null) {
153 LOG.fatal("No ControlParam in config.");
154 }
155
156 // Create a pool of shared applications to handle requests
157 try {
158 initSharedControllerPool();
159 } catch (Exception e) {
160 LOG.fatal("Can't create shared pool of applications", e);
161 throw new RuntimeException("Can't create application!\n" + e.toString());
162 }
163 }
164
165 // StringBuffer message = new StringBuffer();
166 // for (Iterator i = inFailures.iterator(); i.hasNext(); ) {
167 // Object o = i.next();
168 // if (Debug.ON) Debug.assert(o instanceof ValidationFailure);
169 // ValidationFailure failure = (ValidationFailure)o;
170 //
171 // message.append("Failed to set ");
172 // message.append(failure.getProperty());
173 // message.append(" to ");
174 // message.append(failure.getValue());
175 // message.append(" because <I>");
176 // message.append(failure.getException().getLocalizedMessage());
177 // message.append("</I><BR/>");
178 // }
179 //
180 // ViewContext.getViewContext().showError("Validation failed", message.toString());
181 // return true; // the request has been completed
182 // }
183
184
185 /***
186 * Call from a Controller instead of showing a View to force an internal
187 * redirect. Pass a HashMap of form parameters for the new request.
188 *
189 * @param inFormParameters TODO: Describe the Parameter
190 */
191 public static void redirect(HashMap inFormParameters) {
192 if (Debug.ON) {
193 Debug.assertTrue(ViewContext.getViewContext() instanceof ServletContext);
194 }
195 ServletContext context = (ServletContext) ViewContext.getViewContext();
196
197 context.getServlet().handleRequest(inFormParameters);
198 }
199
200
201 /***
202 * Copy references to all form parameters into a mutable Map. If a parameter
203 * has multiple values then the parameter value will be a String[] else a
204 * String. <P>
205 *
206 * This is a useful place to insert default values for missing parameters,
207 * for example to map .../servlet/MyServlet onto some default "action" and
208 * "view" by inserting those default parameters into the returned HashMap if
209 * missing in incoming request. </P>
210 *
211 * @param inRequest find parameters in this request
212 * @return Map containing references to all form parameters, either String
213 * or String[] if multiple values.
214 */
215 protected HashMap getFormParameters(HttpServletRequest inRequest) {
216 HashMap result = new HashMap();
217 for (Enumeration e = inRequest.getParameterNames(); e.hasMoreElements(); ) {
218
219 Object o = e.nextElement();
220 if (Debug.ON) {
221 Debug.assertTrue(o instanceof String);
222 }
223 String name = (String) o;
224
225 o = inRequest.getParameterValues(name);
226 if (Debug.ON) {
227 Debug.assertTrue(o instanceof String[], "not String[]: " + o);
228 }
229 String[] values = (String[]) o;
230 if (Debug.ON) {
231 Debug.assertTrue(values.length >= 1);
232 }
233
234 if (values.length == 1) {
235 result.put(name, values[0]);
236 } else {
237 result.put(name, values);
238 }
239 }
240 return result;
241 }
242
243
244 /***
245 * This implementation maps GET requests onto POST requests.
246 *
247 * @param req TODO: Describe the Parameter
248 * @param resp TODO: Describe the Parameter
249 * @throws ServletException TODO: Describe the Exception
250 * @throws IOException TODO: Describe the Exception
251 */
252 protected void doGet(HttpServletRequest req, HttpServletResponse resp)
253 throws ServletException, IOException {
254 if (LOG.isDebugEnabled()) {
255 LOG.debug("doGet: " + req + ", " + resp);
256 }
257 doPost(req, resp);
258 }
259
260
261 /***
262 * Default implementation uses shared application instances.
263 *
264 * @param req TODO: Describe the Parameter
265 * @param resp TODO: Describe the Parameter
266 * @throws ServletException TODO: Describe the Exception
267 * @throws IOException TODO: Describe the Exception
268 */
269 protected void doPost(HttpServletRequest req, HttpServletResponse resp)
270 throws ServletException, IOException {
271 // Useful debug output
272 if (LOG.isDebugEnabled()) {
273 LOG.debug("doPost: " + req + ", " + resp);
274 }
275 if (LOG.isDebugEnabled()) {
276 for (Enumeration e = req.getParameterNames(); e.hasMoreElements(); ) {
277 Object o = e.nextElement();
278 LOG.debug("doPost: (" + o + ")=(" + req.getParameter(o.toString()) + ")");
279 }
280 }
281
282 // Copy references to the form parameters into a mutable container
283 HashMap formParameters = getFormParameters(req);
284
285 // Install a ViewContext for this request if one not already set (by subclass override)
286 if (Debug.ON) {
287 Debug.assertTrue(ViewContext.getViewContext() == null || ViewContext.getViewContext() instanceof ServletContext);
288 }
289 if (ViewContext.getViewContext() == null) {
290 ViewContext.setThreadContext(createServletContext(req, resp, formParameters));
291 }
292
293 // Need a 'finally' to clear the ViewContext after handling the request
294 try {
295
296 // Now handle the request using the parameters
297 handleRequest(formParameters);
298
299 } catch (Exception e) {
300 LOG.fatal("doPost failed", e);
301 throw new ServletException(e);
302 } finally {
303 // Clear the ViewContext for this request (Thread)
304 ViewContext.clearThreadContext();
305 }
306 }
307
308
309 /***
310 * @param formParameters TODO: Describe the Parameter
311 * @todo document the method
312 */
313 protected void handleRequest(HashMap formParameters) {
314
315 // Make a Control from the form parameters
316 Control control = createControl(formParameters);
317
318 // Find the View ID from the form parameters
319 String viewID = findViewID(formParameters);
320
321 // Get an application from the shared pool to handle the request
322 if (Debug.ON) {
323 Debug.assertTrue(sharedControllerPool != null, "no controller pool");
324 }
325 Controller applicationController = (Controller) sharedControllerPool.borrowObject();
326 if (Debug.ON) {
327 Debug.assertTrue(applicationController != null, "can't find an applicationController");
328 }
329
330 // Need a finally block to return the shared application to the pool
331 try {
332
333 // Find the Page the user interacted with by the incoming View ID
334 Page page = findPageByID(applicationController, viewID);
335 // If no Page found then try to get a default Page ***** don't like this behaviour... option to invoke an error handler?
336 if (page == null) {
337 page = findDefaultPage(applicationController);
338 }
339
340 if (page != null) {
341
342 // Let the Page populate its bound model if it wants to
343 List failures = page.populateModel(formParameters);
344 if (failures != null) {
345 // Validation errors during the populate are handled here.
346 // On return true the error was completely handled so don't carry on.
347 if (handleValidationFailures(page, failures)) {
348 return;
349 }
350 }
351
352 // Now issue the Control, or call BasicController.startup() if no Control.
353 // Note that ControlExceptions are handled by the normal ViewContext.showError() mechanism.
354 if (control == null) {
355 BasicController controller = (BasicController) page.getController();
356 controller.startup();
357 } else {
358 page.issueControl(control);
359 }
360 }
361
362 // If no View shown so far then force a showView on the active Controller's ServletView
363 if (Debug.ON) {
364 Debug.assertTrue(ViewContext.getViewContext() instanceof ServletContext);
365 }
366 ServletContext context = (ServletContext) ViewContext.getViewContext();
367 if (!context.hasShownView()) {
368 if (LOG.isDebugEnabled()) {
369 LOG.debug("doPost: not shown view so showing: " + page);
370 }
371 ServletView sv = page.getParent();
372 context.showView(sv);
373 }
374 } finally {
375 sharedControllerPool.returnObject(applicationController);
376 }
377 }
378
379
380 /***
381 * Create a ViewContext that will be used for a request: default impl here
382 * returns a new {@link ServletContext}. For example to implement your own
383 * error handling, extend the default ServletContext to override showError,
384 * and then override this method in your servlet subclass to create an
385 * instance of your own ServletContext.
386 *
387 * @param req TODO: Describe the Parameter
388 * @param resp TODO: Describe the Parameter
389 * @param inFormParameters TODO: Describe the Parameter
390 * @return TODO: Describe the Return Value
391 */
392 protected abstract ServletContext createServletContext(HttpServletRequest req, HttpServletResponse resp, HashMap inFormParameters);
393
394
395 /***
396 * <P>
397 *
398 * Return a Control instance from request form parameters getting the
399 * Control ID from the CONTROL_PARAM form value, and setting the
400 * formParameters HashMap as the Control's parameter. Also handles imagemap
401 * requests of the form <CODE>imagename.x=ControlId</CODE>. </P> <P>
402 *
403 * Override this for an application to create a default Control when none is
404 * supplied in the request, eg for a simple home request: <PRE>http://localhost:8080/myapp/MyServlet</PRE>
405 * with no parameters to display the application's home page. But also see
406 * {@link #getFormParameters}. </P>
407 *
408 * @param ioFormParameters request's form parameters.
409 * @return Control instance created from the form parameters
410 */
411 protected Control createControl(HashMap ioFormParameters) {
412
413 Object o = ioFormParameters.get(CONTROL_PARAM);
414 if (o instanceof String[]) {
415 LOG.warn("Multiple Control parameters (using first one): " + o);
416 if (Debug.ON) {
417 Debug.assertTrue(((String[]) o).length > 0);
418 }
419 o = ((String[]) o)[0];
420 }
421
422 if (Debug.ON) {
423 Debug.assertTrue(o == null || o instanceof String);
424 }
425 String controlID = (String) o;
426 if (controlID != null && controlID.length() < 1) {
427 controlID = null;
428 }
429
430 // Got it?
431 if (controlID != null) {
432 return new Control(controlID, ioFormParameters);
433 }
434
435 // Search all params looking for something of the form "name.x" coming from an image button
436 for (Iterator i = ioFormParameters.keySet().iterator(); i.hasNext(); ) {
437 o = i.next();
438 if (o instanceof String) {
439 String name = (String) o;
440 if (name.endsWith(".x")) {
441 controlID = name.substring(0, name.length() - 2);
442 break;
443 }
444 }
445 }
446
447 // Got a Control ID? Then make a Control and remove the foo.x and foo.y parameters
448 if (controlID != null && controlID.length() > 0) {
449 return new Control(controlID, ioFormParameters);
450 }
451
452 // Got no control
453 return null;
454 }
455
456
457 /***
458 * Could be overidden to provide a default ViewID if none in the parameters,
459 * but also see {@link #getFormParameters}.
460 *
461 * @param inParameters TODO: Describe the Parameter
462 * @return TODO: Describe the Return Value
463 */
464 protected String findViewID(HashMap inParameters) {
465 return (String) inParameters.get(VIEW_ID_PARAM);
466 }
467
468
469 /***
470 * Search through application's Controller hierarchy to find a Page matching
471 * the ViewID. Return null if not found.
472 *
473 * @param inRootController TODO: Describe the Parameter
474 * @param inViewID TODO: Describe the Parameter
475 * @return TODO: Describe the Return Value
476 */
477 protected Page findPageByID(Controller inRootController, String inViewID) {
478 if (LOG.isDebugEnabled()) {
479 LOG.debug("findPageByID: " + inRootController + ", " + inViewID);
480 }
481 if (Debug.ON) {
482 Debug.assertTrue(inRootController != null);
483 }
484 if (!(inRootController instanceof BasicController)) {
485 throw new RuntimeException("ScopeServlet relies on application Controllers being instanceof BasicController.");
486 }
487
488 // Search at the current root
489 if (Debug.ON) {
490 Debug.assertTrue(inRootController.getView() == null || inRootController.getView() instanceof ServletView);
491 }
492 ServletView v = (ServletView) inRootController.getView();
493 if (v != null) {
494 Page p = v.findPageByID(inViewID);
495 if (p != null) {
496 return p;
497 }
498 }
499
500 // If not there, recurse through all children
501 for (Iterator i = ((BasicController) inRootController).getChildren().iterator(); i.hasNext(); ) {
502 Object o = i.next();
503 if (Debug.ON) {
504 Debug.assertTrue(o instanceof Controller);
505 }
506 Page result = findPageByID((Controller) o, inViewID);
507 if (result != null) {
508 return result;
509 }
510 }
511
512 // And if not found then return null
513 return null;
514 }
515
516
517 /***
518 * If the request ViewID doesn't match any Page then this provides a default
519 * Page: could be the start page of the application that the user gets to by
520 * invoking the servlet with no parameters. Here returns the first Page
521 * found by a depth-first traversal of the application hierarchy. <P>
522 *
523 * If you manage the {@link #getFormParameters} method to validate form
524 * parameters then this method might never be used. But if there's a single
525 * page that you want to use when an invalid ViewID is passed (eg an error
526 * page) then this is the place to do it. </P> <P>
527 *
528 * Don't like this. Should be allowed to redirect to an error handler on
529 * invalid ViewID? ***** </P>
530 *
531 * @param inRootController TODO: Describe the Parameter
532 * @return TODO: Describe the Return Value
533 */
534 protected Page findDefaultPage(Controller inRootController) {
535 // Search at the current root
536 if (Debug.ON) {
537 Debug.assertTrue(inRootController.getView() == null || inRootController.getView() instanceof ServletView);
538 }
539 ServletView v = (ServletView) inRootController.getView();
540 if (v != null) {
541 Page result = v.getFirstPage();
542 if (result != null) {
543 return result;
544 }
545 }
546
547 for (Iterator i = ((BasicController) inRootController).getChildren().iterator(); i.hasNext(); ) {
548 Object o = i.next();
549 if (Debug.ON) {
550 Debug.assertTrue(o instanceof Controller);
551 }
552 Page result = findDefaultPage((Controller) o);
553 if (result != null) {
554 return result;
555 }
556 }
557
558 // And if not found then return null
559 return null;
560 }
561
562
563 /***
564 * <P>
565 *
566 * Called if an exception is thrown by {@link
567 * org.scopemvc.view.servlet.Page#populateModel Page.populateModel}. </P>
568 * <P>
569 *
570 * Default implementation here puts the List of {@link
571 * org.scopemvc.view.servlet.ValidationFailure}s into the ViewContext under
572 * the {@link #VALIDATION_FAILURES} key for retrieval ({@link
573 * org.scopemvc.controller.basic.ViewContext#getProperty} and handling by
574 * Controllers, ie: <PRE>
575 * protected void doSomeHandler() throws ControlException {
576 * List validationFailures = (List)ViewContext.getViewContext().getProperty(ScopeServlet.VALIDATION_FAILURES);
577 * if (validationFailures != null) {
578 * // TODO: Handle the validation failures
579 * } else {
580 * // TODO: No validation failures so handle the control
581 * }
582 * }
583 * </PRE> </P>
584 *
585 * @param inPage TODO: Describe the Parameter
586 * @param inFailures TODO: Describe the Parameter
587 * @return true if this handler has finished the request, ie the normal
588 * request handler can stop immediately.
589 */
590 protected boolean handleValidationFailures(Page inPage, List inFailures) {
591 if (LOG.isDebugEnabled()) {
592 LOG.debug("handleValidationFailures: " + inFailures);
593 }
594 if (Debug.ON) {
595 Debug.assertTrue(inFailures != null);
596 }
597
598 ViewContext.getViewContext().addProperty(VALIDATION_FAILURES, inFailures);
599 return false;
600 }
601
602
603 /***
604 * <P>
605 *
606 * Override this to create the root application Controller. The application
607 * Controller should setup any child Controllers that it needs to handle
608 * parts of the application for it. </P>
609 *
610 * @return new application Controller
611 * @exception Exception on any failure
612 */
613 protected abstract Controller createApplicationController() throws Exception;
614
615
616 // --------------- Shared controller pool -------------------------
617
618 /***
619 * @todo document the method
620 * @throws Exception TODO: Describe the Exception
621 */
622 protected void initSharedControllerPool() throws Exception {
623 int size = 10;
624 Integer i = ScopeConfig.getInteger("org.scopemvc.controller.servlet.ScopeServlet.maxControllerPoolSize");
625 if (i != null) {
626 size = i.intValue();
627 }
628 sharedControllerPool = new BasicObjectPool(new SharedControllerFactory(), size);
629 }
630
631
632 /***
633 * @author smefroi
634 * @created 05 August 2002
635 * @todo document the class
636 */
637 protected class SharedControllerFactory implements PoolableObjectFactory {
638 /***
639 * @return TODO: Describe the Return Value
640 * @todo document the method
641 */
642 public Object createObject() {
643 try {
644 return createApplicationController();
645 } catch (Exception e) {
646 LOG.fatal("Can't create application Controller", e);
647 return null;
648 }
649 }
650
651 /***
652 * @param object TODO: Describe the Parameter
653 * @todo document the method
654 */
655 public void destroyObject(Object object) {
656 // noop
657 }
658
659 /***
660 * @param object TODO: Describe the Parameter
661 * @todo document the method
662 */
663 public void activateObject(Object object) {
664 // noop
665 }
666
667 /***
668 * @param object TODO: Describe the Parameter
669 * @todo document the method
670 */
671 public void passivateObject(Object object) {
672 // noop
673 }
674 }
675 }
This page was automatically generated by Maven